[RNE Rewrite] Add text and image embeddings pipelines#1292
Conversation
Add int64/Long tensor dtype support and text/image embeddings tasks, hooks, and model registry entries, plus an interactive text-embeddings demo screen in apps/nlp. Closes #1247
model.execute now validates dynamically-shaped forward inputs against the model-declared [min, max, step] bounds exposed by an optional get_dynamic_dims method, instead of requiring an exact shape match; models without it keep exact per-dimension validation. Text embeddings feed the exact token length with no padding, which fixes scale-sensitive pooling heads (e.g. DistilUSE's tanh projection). Point DistilUSE at v0.10.0 (re-exported with get_dynamic_dims).
…mbeddings demo - Simplify text-embeddings cosine to a dot product (all models L2-normalize) and drop redundant inline comments. - Move the get_dynamic_dims / input-validation contract into the ModelHostObject class docs; trim the inline narration in model.cpp. - Add an Image Embeddings example to the computer-vision app: pick two images and compare their CLIP embeddings by cosine similarity.
Rework the computer-vision Image Embeddings screen (based on main's CLIP demo): pick an image and rank editable text labels by CLIP image/text embedding similarity, instead of the uninformative two-image score. Pads the scroll content past the Android nav bar. Point CLIP text + image at v0.10.0 (text re-exported with get_dynamic_dims; image unchanged) and declare the textEmbeddings feature in the app.
- model.{h,cpp}: read get_dynamic_dims once per model and cache it instead
of re-executing the method on every forward() call; reject a present-but-
malformed declaration (wrong dtype/rank/shape, bad min/max/step, or row
count not matching forward's tensor input dims) with an explicit error
instead of silently falling back to exact validation.
- textEmbeddings: throw a clear error when input tokenizes to zero tokens
(was BigInt(undefined)); fix docstring to match no-padding behavior.
- useTextEmbeddings: expose localPath/tokenizerPath like sibling hooks.
- computer-vision: extract shared skImageToBuffer helper, dedup from
classification and imageEmbeddings screens.
8e0f200 to
ede040e
Compare
Rebase onto rne-rewrite adopted #1296's rewritten model.cpp, which delegates tensor dtype/shape checks to tensor::fromJs and already supports RangeDim [min, max, step] bounds. Re-implement variable-length forward inputs on top of it: parse get_dynamic_dims once per method into cached bounds, build a SymbolicShape of RangeDims, and pass it to fromJs. Statically shaped methods keep exact validation.
ede040e to
6c3ccc4
Compare
Align the text/image embeddings tasks with the add-task-pipeline skill (and every other task): allocate the static output tensor in a `[...] as const` array, destructure it, and dispose via `tensors.forEach`.
barhanc
left a comment
There was a problem hiding this comment.
Checked only lib implementation. I will take a look at app code on Monday.
Regarding core/ dynamic input support changes, I've added some suggestions that imo make the code more future-proof and easier to change.
Regarding TS side everything is fine, just some small nits. I was thinking though if we don't want to take the opportunity of this refactor and beef-up the TS embeddings side a bit more, something like unifying both image and text embeddings into one pipeline that on top of exposing simple embed method would also expose methods implementing a small vector search data structure like insert, clear, find, etc. I don't know how much effort would that be so it's your call. The current implementation is correct.
|
@barhanc regarding:
If you think that there are other things to consider regarding these two, please let me know and I will re-think it. In a meantime, I will address inline comments :). |
Address review: parse dynamic input shapes into a SymbolicShape-per-input (1:1 with inputs, empty for non-tensor) via parseDynamicInputShapes, keyed on a per-method companion get_dynamic_dims_<methodName> (int32 [rank,3]). Build the methodName -> shapes map for all methods at construction instead of hardcoding forward; execute just indexes by input position. unwrap now throws std::runtime_error (no Runtime needed).
Address review: - createTextEmbeddings -> createTextEmbedder, createImageEmbeddings -> createImageEmbedder; files textEmbedding.ts/imageEmbedding.ts (no 's'), hooks useTextEmbedder/useImageEmbedder, and *EmbedderModel types - forward/forwardWorklet -> embed/embedWorklet - t-prefix tensor vars (tTokenIds/tAttentionMask); document input truncation - stop exporting tasks from extensions/<domain>/index.ts (drop textEmbedding and the pre-existing tokenization export)
There was a problem hiding this comment.
Why was this change only added to classification when other CV screens use the same pattern?
There was a problem hiding this comment.
It was an unrelated cleanup that I'd only applied here — reverted it to keep this PR scoped to embeddings.
|
Tested apps and they work fine. Also I see that on huggingface we also have LFM text embeddings, why are they not added here? |
|
One more thing, we should probably add some information about the dynamic methods to skills and jsdoc so that agents are aware of the companion method requirement, this is adhoc solution until we have a proper documentation. |
- Add a jsi::JSError overload of unwrap and use it across the JSI host functions (getMethodNames/getMethodMeta/execute), so runtime failures surface as JS errors; the std::runtime_error overload stays for the load-time constructor path where no jsi::Runtime is available. - Validate output placeholders against the tensor's actual produced shape (output.toTensor().sizes()) so dynamically-shaped outputs are supported. - useTextEmbedder: report the model download progress directly instead of averaging with the tiny tokenizer download. - Revert the unrelated classification screen refactor (skImageToBuffer / insets) to keep this PR scoped to embeddings.
Description
Adds text and image embeddings pipelines to the new architecture, achieving parity with the old flow. Embeddings are pure-TypeScript tasks (pooling + L2-norm stay baked into the
.pte): text tokenizes and runsforward; image reuses the existing image preprocessor. To run the existing int64-input embedding models unchanged, this adds anint64/Longtensor dtype to the core (the tensor data path is byte-oriented, so it is a smalldtype.{h,cpp}+tensor.tschange).Text inputs are fed at their exact token length (no padding).
model.executevalidates dynamically-shaped inputs against the[min, max, step]bounds exposed by an optional per-methodget_dynamic_dims_<methodName>companion; methods without it keep exact per-dimension validation. This fixes scale-sensitive pooling heads (e.g. DistilUSE's tanh projection), which padding otherwise corrupts.Includes
createTextEmbedder/createImageEmbeddertasks,useTextEmbedder/useImageEmbedderhooks,models.textEmbeddings/models.imageEmbeddingsregistry entries, an interactive text-embeddings demo inapps/nlp, and a CLIP zero-shot image-embeddings demo inapps/computer-vision.Introduces a breaking change?
Type of change
Tested on
Testing instructions
nlpapp → Text Embeddings: seeds a sentence library; type a query and Find similar to rank by cosine similarity, switch models via the chips. Verified on a physical Android device (arm64): all-MiniLM-L6-v2 returns 384-dim L2-normalized embeddings (~25 ms/forward on XNNPACK); DistilUSE ranks correctly with a wide similarity spread (previously compressed by padding).computer-visionapp → Image Embeddings: pick an image and rank editable text labels via CLIP zero-shot (image vs. text embeddings). Verified on device.Screenshots
Related issues
#1247
Checklist
Additional notes
All text- and image-embedding models are re-exported with the per-method
get_dynamic_dims_<methodName>companion and pinned tov0.10.0: all-MiniLM-L6-v2, all-mpnet-base-v2, multi-qa MiniLM/MPNet, paraphrase-ML, DistilUSE, and CLIP text (plus the DistilUSE/paraphrase CoreML variants). CLIP image has a static input, so it needs no companion and is unchanged.